package eu.vranckaert.calendar.util;

import android.annotation.SuppressLint;
import android.content.Context;
import android.util.SparseArray;
import org.joda.time.DateTimeFieldType;
import org.joda.time.LocalDate;

import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.Date;
import java.util.Locale;

/**
 * User: Dirk Vranckaert
 * Date: 13/02/13
 * Time: 13:17
 */
public class DateUtil {
    public static class DateTimeConverter {
        /**
         * Converts a date to an entire date-time-string based on the users locale in the context.
         * @param date The date to convert.
         * @param dateFormat The date format to use.
         * @param context The context in which the locale is stored.
         * @return The formatted string.
         */
        public static final String convertDateTimeToString(Date date, DateFormat dateFormat, Context context) {
            Locale locale = ContextUtils.getCurrentLocale(context);
            return convertDateToString(date, dateFormat, locale)
                    + " "
                    + convertTimeToString(date, context);
        }

        /**
         * Converts a certain date to a date-string based on the users locale in the context.
         * @param date The date to convert.
         * @param format The date format to use.
         * @param context The context in which the locale is stored.
         * @return The formatted string.
         */
        public static final String convertDateToString(Date date, DateFormat format, Context context) {
            Locale locale = ContextUtils.getCurrentLocale(context);
            return convertDateToString(date, format, locale);
        }

        /**
         * Converts a certain date to a date-string based on the users locale.
         * @param date The date to convert.
         * @param format The date format to use.
         * @param locale The users locale.
         * @return The formatted string.
         */
        public static final String convertDateToString(Date date, DateFormat format, Locale locale) {
            java.text.DateFormat dateFormat = java.text.DateFormat.getDateInstance(format.getStyle(), locale);
            return dateFormat.format(date);
        }

        /**
         * Converts a certain date to a time-string based on the users locale in the context.
         * @param date The date to convert.
         * @return The formatted string.
         */
        public static final String convertTimeToString(Date date, Context context) {
            return convertTimeToString(context, date);
        }

        /**
         * Converts a certain date to a time-string based on the users locale.
         * @param ctx The context.
         * @param date The date to convert.
         * @return The formatted string.
         */
        @SuppressLint("SimpleDateFormat")
		public static final String convertTimeToString(Context ctx, Date date) {
            String separator = ":";
            String minutes = "mm";
            String hours24 = "HH";
            String hours12 = "hh";
            String amPmMarker = " a";

            String dateFormat = separator + minutes;

            if (DateUtil.System.is24HourClock(ctx)) {
                dateFormat = hours24 + dateFormat;
            } else {
                dateFormat = hours12 + dateFormat + amPmMarker;
            }

            SimpleDateFormat sdf = new SimpleDateFormat(dateFormat);
            return sdf.format(date);
        }

        /**
         * Converts a date-time to new {@link java.util.Date} instance with the hours, minutes, seconds and milliseconds set to 0.
         * @param date The date to convert.
         * @return The converted date instance.
         */
        public static Date convertDateTimeToDateOnly(Date date) {
            Calendar day = Calendar.getInstance();
            day.setTime(date);
            day.set(Calendar.HOUR_OF_DAY, day.getActualMinimum(Calendar.HOUR_OF_DAY));
            day.set(Calendar.MINUTE, day.getActualMinimum(Calendar.MINUTE));
            day.set(Calendar.SECOND, day.getActualMinimum(Calendar.SECOND));
            day.set(Calendar.MILLISECOND, day.getActualMinimum(Calendar.MILLISECOND));
            return day.getTime();
        }

        /**
         * Merges two {@link java.util.Date} objects together. From the first parameter only the day-fields are kept (like month,
         * year, day), from the second parameter only the time (hour and minutes) are kept. The seconds and milliseconds
         * are set to zero.
         * @param day The day entity.
         * @param time The time entity.
         * @return A new {@link java.util.Date} instance that contains the date and time that is merged from both objects.
         */
        public static Date createDateFromDayAndTime(Date day, Date time) {
            Calendar dateTime = Calendar.getInstance();
            dateTime.setTime(day);

            Calendar timeCalendar = Calendar.getInstance();
            timeCalendar.setTime(time);

            dateTime.set(Calendar.HOUR_OF_DAY, timeCalendar.get(Calendar.HOUR_OF_DAY));
            dateTime.set(Calendar.MINUTE, timeCalendar.get(Calendar.MINUTE));
            dateTime.set(Calendar.SECOND, timeCalendar.getActualMinimum(Calendar.SECOND));
            dateTime.set(Calendar.MILLISECOND, timeCalendar.getActualMinimum(Calendar.MILLISECOND));

            return dateTime.getTime();
        }

        /**
         * Checks if the provided date is somewhere on today.
         * @param day The day to check for.
         * @return Returns {@link Boolean#TRUE} if the provided date is somewhere today, {@link Boolean#FALSE} if not.
         */
        public static boolean isDayToday(Date day) {
            Calendar today = Calendar.getInstance();

            Calendar dayCalendar = Calendar.getInstance();
            dayCalendar.setTime(day);

            if (today.get(Calendar.YEAR) == dayCalendar.get(Calendar.YEAR)
                    && today.get(Calendar.MONTH) == dayCalendar.get(Calendar.MONTH)
                    && today.get(Calendar.DAY_OF_MONTH) == dayCalendar.get(Calendar.DAY_OF_MONTH)) {
                return true;
            }

            return false;
        }

        /**
         * Checks if two dates are occurring on the same day (year, month and day of month).
         * @param day1 The first date.
         * @param day2 The second date.
         * @return True if on the same day, false if not.
         */
        public static boolean isSameDay(Date day1, Date day2) {
            Calendar cal1 = Calendar.getInstance();
            cal1.setTime(day1);

            Calendar cal2 = Calendar.getInstance();
            cal2.setTime(day2);

            if (cal1.get(Calendar.YEAR) == cal2.get(Calendar.YEAR)
                    && cal1.get(Calendar.MONTH) == cal2.get(Calendar.MONTH)
                    && cal1.get(Calendar.DAY_OF_MONTH) == cal2.get(Calendar.DAY_OF_MONTH)) {
                return true;
            }

            return false;
        }



        /**
         * Calculates the boundaries of a week (the date the week is starting and ending on), based on the week
         * difference that is provided. If zero it will be the current week boundaries. The week difference can be zero,
         * negative or positive. A week difference of -1 for example will calculate the week boundaries for last week.
         * The method will take into account the preference for what day the week should start on.
         * @param weekDiff The week difference to calculate the boundaries from.
         * @param weekStartsOn The day the week starts on. 1 for monday.
         * @param ctx The context.
         * @return A map containing two keys: "1" for the first day of the week and "2" for the last day of the week.
         */
        public static SparseArray<Date> calculateWeekBoundaries(final int weekDiff, final int weekStartsOn, final Context ctx) {
            return calculateWeekBoundaries(weekDiff, new Date(), weekStartsOn, ctx);
        }

        /**
         * Calculates the boundaries of a week (the date the week is starting and ending on), based on the week
         * difference that is provided. If zero it will be the week boundaries of the specified date. The week
         * difference can be zero, negative or positive. A week difference of -1 for example will calculate the week
         * boundaries for the week before the specified date. The method will take into account the preference for what
         * day the week should start on.
         * @param weekDiff The week difference to calculate the boundaries from.
         * @param date The date to start calculating from.
         * @param weekStartsOn The day the week starts on. 1 for monday.
         * @param ctx The context.
         * @return A {@link android.util.SparseArray} the first day of the week and the last day.
         */
        public static SparseArray<Date> calculateWeekBoundaries(final int weekDiff, final Date date, int weekStartsOn, final Context ctx) {
            Date dateWithWeeks = addWeeksToDate(date, weekDiff);

            LocalDate startDate = new LocalDate(dateWithWeeks);
            LocalDate firstDayOfWeek = new LocalDate(dateWithWeeks);

            firstDayOfWeek = firstDayOfWeek.withDayOfWeek(weekStartsOn);
            if (firstDayOfWeek.isAfter(startDate)) {
                DateTimeFieldType weekOfWeekyear = DateTimeFieldType.weekOfWeekyear();
                int weekOfYear = firstDayOfWeek.get(weekOfWeekyear);
                int newWeekOfYear = weekOfYear - 1;
                if (newWeekOfYear <= 0) {
                    newWeekOfYear = 52 - newWeekOfYear;
                    firstDayOfWeek = firstDayOfWeek.withYear(firstDayOfWeek.getYear()-1);
                }
                firstDayOfWeek = firstDayOfWeek.withWeekOfWeekyear(newWeekOfYear);
                firstDayOfWeek = firstDayOfWeek.withDayOfWeek(weekStartsOn);
            }

            Calendar lastDayOfWeek = Calendar.getInstance();
            lastDayOfWeek.setTime(addWeeksToDate(firstDayOfWeek.toDate(), 1));
            lastDayOfWeek.add(Calendar.DAY_OF_YEAR, -1);

            SparseArray<Date> result = new SparseArray<Date>();



            result.put(0, DateUtil.DateTimeConverter.convertDateTimeToDateOnly(firstDayOfWeek.toDate()));
            result.put(1, DateUtil.DateTimeConverter.convertDateTimeToDateOnly(lastDayOfWeek.getTime()));

            return result;
        }

        /**
         * Add an amount of weeks to a certain date. If amount of weeks to add is negative it will be subtracted.
         * @param date The {@link java.util.Date} instance to calculate on.
         * @param weekDiff The number of weeks to add to the provided date.
         * @return The {@link java.util.Date} with the weeks added or subtracted.
         */
        private static Date addWeeksToDate(Date date, int weekDiff) {
            int daysInOneWeek = Calendar.getInstance().getMaximum(Calendar.DAY_OF_WEEK);
            int daysDiff = weekDiff * daysInOneWeek;

            Calendar cal = Calendar.getInstance();
            cal.setTime(date);
            cal.add(Calendar.DAY_OF_YEAR, daysDiff);

            return cal.getTime();
        }

        /**
         * Checks if a certain date is occuring this week.
         * @param day The day to check.
         * @param weekStartsOn The day the week starts on. 1 for monday.
         * @param context The context.
         * @return {@link Boolean#TRUE} if it's this week, {@link Boolean#FALSE} if not.
         */
        public static boolean isDayThisWeek(Date day, int weekStartsOn, Context context) {
            SparseArray<Date> weekBoundaries = calculateWeekBoundaries(0, new Date(), weekStartsOn, context);
            Date firstDayOfWeek = weekBoundaries.get(0);
            Date lastDayOfWeek = weekBoundaries.get(1);

            day = DateUtil.DateTimeConverter.convertDateTimeToDateOnly(day);

            if (DateUtil.DateTimeConverter.isSameDay(day, firstDayOfWeek)
                    || DateUtil.DateTimeConverter.isSameDay(day, lastDayOfWeek)
                    || ( day.after(firstDayOfWeek) && day.before(lastDayOfWeek) ) ) {
                return true;
            }

            return false;
        }

        public static boolean isDayInSameWeek(Date day1, Date day2, int weekStartsOn, Context context) {
            SparseArray<Date> weekBoundariesDay1 = calculateWeekBoundaries(0, day1, weekStartsOn, context);
            SparseArray<Date> weekBoundariesDay2 = calculateWeekBoundaries(0, day2, weekStartsOn, context);

            Date firstDayOfWeekDay1 = weekBoundariesDay1.get(0);
            Date lastDayOfWeekDay1 = weekBoundariesDay1.get(1);
            Date firstDayOfWeekDay2 = weekBoundariesDay2.get(0);
            Date lastDayOfWeekDay2 = weekBoundariesDay2.get(1);

            if (DateUtil.DateTimeConverter.isSameDay(firstDayOfWeekDay1, firstDayOfWeekDay2)
                    && DateUtil.DateTimeConverter.isSameDay(lastDayOfWeekDay1, lastDayOfWeekDay2)) {
                return true;
            }

            return false;
        }

        public static boolean isDayInSameMonth(Date day1, Date day2, Context context) {
            Calendar cal1 = Calendar.getInstance();
            cal1.setTime(day1);

            Calendar cal2 = Calendar.getInstance();
            cal2.setTime(day2);

            if (cal1.get(Calendar.YEAR) == cal2.get(Calendar.YEAR)
                    && cal1.get(Calendar.MONTH) == cal2.get(Calendar.MONTH)) {
                return true;
            }

            return false;
        }
        
        public static int numberOfDaysBetweenDates(final Date startDate, final Date endDate) {
        	return (int)( (endDate.getTime() - startDate.getTime()) / (1000 * 60 * 60 * 24));
        }
    }

    public static class System {
        /**
         * Find out if a 24 hours clock is preferred or not. The check will be done based on the user's locale.
         * @param context The context to find the uers's locale.
         * @return {@link Boolean#TRUE} if the 24 hours format is preferred. {@link Boolean#FALSE} if the AM/PM notation
         * is preferred.
         */
        public static boolean is24HourClock(Context context) {
            Locale locale = ContextUtils.getCurrentLocale(context);
            return is24HourClock(locale);
        }

        /**
         * Find out, based on the user's locale, if a 24 hours clock is preferred or not.
         * @param locale The user's locale.
         * @return {@link Boolean#TRUE} if the 24 hours format is preferred. {@link Boolean#FALSE} if the AM/PM notation
         * is preferred.
         */
		public static boolean is24HourClock(Locale locale) {
            java.text.DateFormat dateFormat = java.text.DateFormat.getTimeInstance(java.text.DateFormat.FULL, locale);
            @SuppressWarnings("unused")
            String t = dateFormat.format(new Date());

            java.text.DateFormat stdFormat = java.text.DateFormat.getTimeInstance(java.text.DateFormat.SHORT, Locale.US);
            java.text.DateFormat localeFormat = java.text.DateFormat.getTimeInstance(java.text.DateFormat.LONG, locale);
            String check = "";
            try {
                check = localeFormat.format(stdFormat.parse("7:00 PM"));
            } catch (ParseException ignore) {
                return false;
            }
            boolean is24HourClock = check.contains("19");

            return is24HourClock;
        }
    }
}
